/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once
#include "pch.h"
#include <journal/views/grid_page_layout.h>

namespace journal { namespace view {

grid_page_layout::grid_page_layout(
    const size_t tileCount,
    const std::shared_ptr<view::inotebook>& notebook
    ) : _tileCount(tileCount), _notebook(notebook)
{
    b::float32 windowWorldWidth = constants::shellService()->windowSize().x / constants::shellService()->logicalDpi();
    b::float32 aspectRatioPixel = constants::shellService()->windowSize().x / constants::shellService()->windowSize().y;

    // calculate focus from tile count
    _focus.y = tileCount * constants::general::pageSlotWorldDimensions().y;
    _focus.x = _focus.y * aspectRatioPixel;

    // calculate scale from focus
    _scale = windowWorldWidth / _focus.x;
}

void grid_page_layout::setReference(
    const size_t referencePageNumber, 
    const m::point3d referenceWorld
    )
{
    // calculate the world origin (at the center of page zero)
    _originWorld = m::point3d(0.f, 0.f, 0.f);
    m::point3d pagePosition = this->calculatePagePosition(referencePageNumber);
    _originWorld.x = (referenceWorld.x - pagePosition.x);
    _originWorld.y = (referenceWorld.y - pagePosition.y);
    _originWorld.z = (referenceWorld.z - pagePosition.z);

    _referencePageNumber = referencePageNumber;
    _referenceWorld = referenceWorld;
}

m::point2d grid_page_layout::calculateCameraOffsetXY(const size_t pageNumber)
{
    m::point3d cameraPositionNormal;
    this->pageNumberToNormal(pageNumber, &cameraPositionNormal);

    cameraPositionNormal.x += 0.5f;
    cameraPositionNormal.y = _tileCount / 2.f;

    m::point3d cameraPosition;
    this->normalToWorld(cameraPositionNormal, &cameraPosition);

    m::point2d offsetXY(cameraPosition.x, cameraPosition.y);
    this->constrainOffsetXY(offsetXY);
    return offsetXY;
}

m::point3d grid_page_layout::calculatePagePosition(const size_t pageNumber)
{
    // calculate final position
    m::point3d normal;
    this->pageNumberToNormal(pageNumber, &normal);

    m::point3d world;
    this->normalToWorld(normal, &world);

    world.x += constants::general::pageSlotWorldDimensions().x / 2.f;
    world.y -= constants::general::pageSlotWorldDimensions().y / 2.f;
    world.z = 0.f;

    return world;
}

size_t grid_page_layout::calculatePageNumber(m::point3d& position)
{
    m::point3d normal;
    this->worldToNormal(position, &normal);

    size_t pageNumber;
    this->normalToPageNumber(normal, &pageNumber);
    return pageNumber;
}

void grid_page_layout::constrainOffsetXY(m::point2d& offsetXY)
{
    // y is always fixed to the middle of the range
    m::point3d normal(0.f, _tileCount / 2.f, 0.f);
    m::point3d world;
    this->normalToWorld(normal, &world);

    offsetXY.y = world.y;

    b::float32 x0 = this->calculatePagePosition(0).x - constants::general::pageSlotWorldDimensions().x / 2.f;
    x0 += _focus.x / 2.f;
    if (offsetXY.x < x0)
    {
        offsetXY.x = x0;
    }
    else 
    {
        b::float32 xN = this->calculatePagePosition(_notebook.lock()->model()->pageCount() - 1).x + constants::general::pageSlotWorldDimensions().x / 2.f;
        if (_focus.x / 2.f >= xN - x0)
        {
            offsetXY.x = x0;
        }
        else
        {
            xN -= _focus.x / 2.f;
            if (offsetXY.x > xN)
            {
                offsetXY.x = xN;
            }
        }
    }
}

void grid_page_layout::constrainScale(b::float32& scale)
{
    scale = max(scale, 0.01f);
}

void grid_page_layout::calculatePageSet(
    const m::vector2d& offsetXY,
    const m::vector2d& focus,
    const page_set& currentPageSet,
    size_t currentPage,
    page_set* pageSet, 
    size_t* pageNumber
    )
{
    m::rectangle cameraBoundsNormal;
    this->calculateCameraBoundsNormal(offsetXY, focus, &cameraBoundsNormal);

    m::rectangle boundsNormal;
    this->calculateBoundsNormal(&boundsNormal);

    pageSet->minPage = NO_PAGE;
    pageSet->maxPage = NO_PAGE;
    pageSet->changeNumber = currentPageSet.changeNumber + 1;

    m::rectangle intersection;
    if (this->calculateIntersection(boundsNormal, cameraBoundsNormal, &intersection))
    {
        this->normalToPageNumber(
            m::point3d(max(intersection.left, 0.f), max(intersection.top, 0.f), 0.f),
            &pageSet->minPage
            );

        this->normalToPageNumber(
            m::point3d(max(intersection.right, 0.f), max(intersection.bottom, 0.f), 0.f),
            &pageSet->maxPage
            );
    }

    // calculate new page number at the center of the camera view
    m::point3d cameraCenterNormal(
        cameraBoundsNormal.left + (cameraBoundsNormal.right - cameraBoundsNormal.left) / 2,
        cameraBoundsNormal.top + (cameraBoundsNormal.bottom - cameraBoundsNormal.top) / 2,
        0.f
        );

    this->normalToPageNumber(cameraCenterNormal, pageNumber);
}

bool grid_page_layout::calculateIntersection(const m::rectangle& r1, const m::rectangle& r2, m::rectangle* rout)
{
    if (this->calculateIntersection(r1.left, r1.right, r2.left, r2.right, &(rout->left), &(rout->right)))
    {
        return this->calculateIntersection(r1.top, r1.bottom, r2.top, r2.bottom, &(rout->top), &(rout->bottom));
    }

    return false;
}

bool grid_page_layout::calculateIntersection(
    const b::float32 left1, 
    const b::float32 right1, 
    const b::float32 left2, 
    const b::float32 right2,
    b::float32* leftout,
    b::float32* rightout
    )
{
    bool returnValue = false;

    if (left1 <= left2)
    {
        if (right1 >= left1)
        {
            returnValue = true;
            *leftout = left2;
            if (right1 <= right2)
            {
                *rightout = right1;
            }
            else
            {
                *rightout = right2;
            }
        }
    }
    else // left1 > left2
    {
        if (right2 >= left1)
        {
            returnValue = true;
            *leftout = left1;
            if (right1 <= right2)
            {
                *rightout = right1;
            }
            else
            {
                *rightout = right2;
            }
        }
    }

    return returnValue;
}

void grid_page_layout::normalToWorld(const m::point3d& pointNormal, m::point3d* pointWorld)
{
    pointWorld->x = (pointNormal.x * constants::general::pageSlotWorldDimensions().x) - (constants::general::pageSlotWorldDimensions().x / 2.f);
    pointWorld->y = -(pointNormal.y * constants::general::pageSlotWorldDimensions().y) + (constants::general::pageSlotWorldDimensions().y / 2.f);
    pointWorld->z = pointNormal.z;

    pointWorld->x += _originWorld.x;
    pointWorld->y += _originWorld.y;
    pointWorld->z += _originWorld.z;
}

void grid_page_layout::worldToNormal(const m::point3d& pointWorld, m::point3d* pointNormal)
{
    m::point3d world = pointWorld;
    world.x = pointWorld.x - _originWorld.x;
    world.y = pointWorld.y - _originWorld.y;
    world.z = pointWorld.z - _originWorld.z;

    pointNormal->x = (world.x + (constants::general::pageSlotWorldDimensions().x / 2.f)) / constants::general::pageSlotWorldDimensions().x;
    pointNormal->y = (world.y - (constants::general::pageSlotWorldDimensions().y / 2.f)) / constants::general::pageSlotWorldDimensions().y;
    pointNormal->y *= -1.f;
    pointNormal->z = 0;
}

void grid_page_layout::pageNumberToNormal(const size_t pageNumber, m::point3d* pointNormal)
{
    pointNormal->x = static_cast<b::float32>(pageNumber / _tileCount);
    pointNormal->y = static_cast<b::float32>(pageNumber % _tileCount);
    pointNormal->z = 0;
}

void grid_page_layout::normalToPageNumber(const m::point3d& pointNormal, size_t* pageNumber)
{
    // NOTE: this code can be improved to calculate the page number on negative offsets too
    // but since this is something that should never really happen as of now I am not bothering
    if (pointNormal.y < 0) *pageNumber = NO_PAGE;
    else if (pointNormal.x < 0) *pageNumber = NO_PAGE;
    else
    {
        size_t x = static_cast<size_t>(floor(pointNormal.x));
        size_t y = static_cast<size_t>(floor(pointNormal.y));

        size_t pageCount = _notebook.lock()->model()->pageCount();
        if (x >= pageCount / _tileCount) x = pageCount / _tileCount; else if (x < 0) x = 0;
        if (y >= _tileCount) y = _tileCount - 1; else if (y < 0) y = 0;

        *pageNumber = x * _tileCount + y;
        if (*pageNumber > pageCount - 1) *pageNumber = pageCount - 1;
    }
}

void grid_page_layout::calculateCameraBoundsNormal(
    const m::vector2d& offsetXY,
    const m::vector2d& focus,
    m::rectangle* bounds
    )
{
    m::point3d cameraWorld(
        offsetXY.x,
        offsetXY.y,
        0.f
        );

    m::point2d halfFocus(focus.x / 2.f, focus.y / 2.f);

    m::point3d topLeft(
        offsetXY.x - halfFocus.x,
        offsetXY.y + halfFocus.y,
        0.f
        );

    m::point3d bottomRight(
        offsetXY.x + halfFocus.x,
        offsetXY.y - halfFocus.y,
        0.f
        );

    m::point3d topLeftNormal;
    this->worldToNormal(topLeft, &topLeftNormal);

    m::point3d bottomRightNormal;
    this->worldToNormal(bottomRight, &bottomRightNormal);

    // camera: top-left, bottom-right
    bounds->left = topLeftNormal.x;
    bounds->top = topLeftNormal.y;
    bounds->right = bottomRightNormal.x;
    bounds->bottom = bottomRightNormal.y;
}

void grid_page_layout::calculateBoundsNormal(m::rectangle* bounds)
{
    size_t pageCount = _notebook.lock()->model()->pageCount();

    bounds->left = 0.f;
    bounds->top = 0.f;
    bounds->right = ceil((b::float32)pageCount / _tileCount);
    bounds->bottom = (pageCount > 0) ? _tileCount : 0.f;
}

void grid_page_layout::calculateScroll(
    const m::vector2d& offsetXY,
    const m::vector2d& focus,
    m::point2d& dimensions,
    m::point2d& position
    )
{
    m::rectangle cameraBoundsNormal;
    this->calculateCameraBoundsNormal(offsetXY, focus, &cameraBoundsNormal);

    m::rectangle boundsNormal;
    this->calculateBoundsNormal(&boundsNormal);

    b::float32 totalWidth = boundsNormal.right - boundsNormal.left;
    b::float32 totalHeight = boundsNormal.bottom - boundsNormal.top;

    dimensions.x = (cameraBoundsNormal.right - cameraBoundsNormal.left) / totalWidth;
    dimensions.y = (cameraBoundsNormal.bottom - cameraBoundsNormal.top) / totalHeight;

    position.x = (cameraBoundsNormal.left) / totalWidth;
    position.y = (cameraBoundsNormal.top) / totalHeight;
}

}} // namespace journal::view